"""
文件 | 业务层
"""

import shutil
import traceback
from functools import partial

from fastapi import UploadFile
from starlette.datastructures import Headers

import utils.enum_utils as enum
from biz.base_biz import (
    CreatorBase,
    DeleterBase,
    GetterBase,
    UpdatorBase,
    ValidatorBase,
)
from components.global_param import get_user_id
from components.logger import log
from components.thread_pool import async_io_task
from config import settings
from mapping import FileMapping
from models.other import FileStream
from models.po import (
    FileGridPO,
    FilePO,
    ProcessPO,
    UserPO,
)
from models.request import (
    FileBatchDownloadRequest,
    FileDetailRequest,
    FileListRequest,
    FileUpdateRequest,
    FileUploadRequest,
)
from models.vo import (
    FileBatchDownloadVO,
    FileDetailVO,
)
from my_typing import *
from service import (
    FileGridService,
    FileService,
)
from utils.data_utils.special import ObjectIdStr
from utils.decorator_utils import task_exc_catcher
from utils.exception_utils import BusinessError
from utils.file_utils import zip_directory
from utils.mock_utils import DATETIME


class Model:
    """ 模型信息 """

    ID_FIELD: str = "file_id"
    NAME: str = "文件"
    PO = FilePO
    SERVICE = FileService


class FileValidator(ValidatorBase):
    """ 文件 | 校验器 """

    model_id_field = Model.ID_FIELD
    model_name = Model.NAME
    model_po = Model.PO
    model_service = Model.SERVICE

    # -------------------- is_xxx --------------------

    @staticmethod
    def is_default_file(file_id: ObjectIdStr | None) -> bool:
        """
        是否是 | 默认文件

        Args:
            file_id: 文件 ID
        """

        file_po: FilePO | None = FileService.get_one_by_id(file_id)

        return (
            file_po
            and file_po.is_default
        )

    @staticmethod
    def is_cited(file_id: ObjectIdStr | None) -> bool:
        """
        是否被引用

        1. 用户头像
        2. TODO: 文件使用场景未来待扩充
        """

        from biz.user_biz import UserGetter

        # 1. 用户头像
        user_po: UserPO | None = UserGetter.get_user_by_avatar_id(file_id)

        used_list = [
            user_po,
        ]
        """ 已使用的列表（待扩充） """

        return any(used_list)

    # -------------------- validate_xxx --------------------

    @staticmethod
    def validate_file_size(file: UploadFile | None) -> None:
        """ 校验 | 文件大小是否超出限制 """

        if not file:
            return None

        if (
            file.size
            and file.size > settings.LIMIT.MAX_FILE_SIZE
        ):
            raise BusinessError("文件大小超出限制")

    @staticmethod
    def validate_filename_exist(
        filename: StrOrNone,
        user_id: ObjectIdStr | None,
        file_id: ObjectIdStr | None = None,
    ) -> None:
        """
        校验 | 文件名是否存在

        Args:
            filename: 文件名
            user_id: 当前用户 ID
            file_id: 文件 ID（更新的时候需要传）
        """

        # 获取当前用户的同名的文件
        file_po: FilePO | None = FileService.get_one_by_filename(filename, user_id=user_id)

        if not file_po:
            return None

        if (
            not file_id
            or file_id != file_po.id
        ):
            raise BusinessError(f"文件名 <{filename}> 已存在")

    @classmethod
    def validate_default_file(cls, file_id: ObjectIdStr | None) -> None:
        """
        校验 | 是否是默认文件

        Args:
            file_id: 文件 ID
        """

        if cls.is_default_file(file_id):
            raise BusinessError(f"文件 ID <{file_id}> 为默认文件")

    @classmethod
    def validate_in_use(cls, file_id: ObjectIdStr | None) -> None:
        """
        校验 | 是否是正在被使用的文件

        Args:
            file_id: 文件 ID
        """

        if cls.is_cited(file_id):
            raise BusinessError(f"文件 ID <{file_id}> 正在被使用")

    # -------------------- validate_crud --------------------

    @classmethod
    def validate_create(cls, request: FileUploadRequest) -> None:
        """
        校验 | 创建

        1. 文件大小是否超出限制
        2. 文件名是否重复
        """

        cls.validate_union(
            {
                "file": request.file,
                "filename": request.file.filename,
                "user_id": get_user_id(),
            },
            cls.validate_file_size,
            cls.validate_filename_exist,
        )

    @classmethod
    def validate_update(cls, request: FileUpdateRequest) -> None:
        """
        校验 | 更新

        1. 文件是否存在
        2. 文件是否是默认文件
        3. 文件是是当前用户的
        4. 文件名是否重复
        """

        cls.validate_union(
            {
                "id": request.file_id,
                "filename": request.filename,
                "file_id": request.file_id,
                "user_id": get_user_id(),
            },
            cls.validate_exist,
            cls.validate_default_file,
            cls.validate_mine,
            cls.validate_filename_exist,
        )

    @classmethod
    def validate_delete(cls, request: FileDetailRequest) -> None:
        """
        校验 | 删除

        1. 文件是否存在
        2. 文件是否为默认文件
        3. 文件是否是当前用户的
        4. 文件是否正在被使用
        """

        cls.validate_union(
            {
                "id": request.file_id,
                "file_id": request.file_id,
                "user_id": get_user_id(),
            },
            cls.validate_exist,
            cls.validate_default_file,
            cls.validate_mine,
            cls.validate_in_use,
        )

    @classmethod
    def validate_batch_download(cls, request: FileBatchDownloadRequest) -> None:
        """
        校验 | 批量下载

        1. 文件是否存在
        2. 文件是否为默认文件
        3. 文件是否是当前用户的
        """

        user_id: ObjectIdStr = get_user_id()

        for _file_id in request.file_id_list:
            cls.validate_union(
                {
                    "id": _file_id,
                    "file_id": _file_id,
                    "user_id": user_id,
                },
                cls.validate_exist,
                cls.validate_default_file,
                cls.validate_mine,
            )


class FileGetter(GetterBase):
    """ 文件 | 获取器 """

    model_mapping = FileMapping
    model_validator = FileValidator

    @classmethod
    def get_list_query(cls, request: FileListRequest) -> dict:
        condition = cls.Condition({"is_default": False})

        condition.add_eq("is_public", value=request.is_public)
        condition.add_regex("filename", value=request.filename)

        if request.is_public is False:
            condition.add_eq("user_id", value=get_user_id())

        return condition.get_query()

    @staticmethod
    def get_file_view_url(file_id: ObjectIdStr) -> str:
        """ 获取文件预览 URL """

        from main import app

        file_view_path: str = str(app.url_path_for("file_view", file_id=str(file_id)))
        backend_url: str = settings.BACKEND.URL

        return backend_url + file_view_path

    @staticmethod
    def get_file_download_url(file_id: ObjectIdStr) -> str:
        """ 获取文件下载 URL """

        from main import app

        file_view_path: str = str(app.url_path_for("file_download", file_id=str(file_id)))
        backend_url: str = settings.BACKEND.URL

        return backend_url + file_view_path


class FileParser:
    """ 文件 | 解析器 """

    @staticmethod
    def parse_file_grid(
        file: UploadFile,
        file_id: ObjectIdStr,
    ) -> FileGridPO:
        """ 解析文件存储 """

        return FileGridPO(
            id=file_id,
            content=file.file.read(),
        )

    @staticmethod
    def parse_file(request: FileUploadRequest) -> FilePO:
        """ 解析文件 """

        raw_filename: str = request.file.filename or ""

        return FilePO(
            user_id=get_user_id(),
            raw_filename=raw_filename,
            filename=request.filename or raw_filename,
            size=request.file.size,
            content_type=request.file.content_type,
            suffix=raw_filename.split(".")[-1],
            is_public=request.is_public,
        )


class FileUploader(CreatorBase):
    """ 文件 | 上传器 """

    @staticmethod
    def _create_data(request: FileUploadRequest) -> FilePO:
        """ 创建数据 """

        file_po: FilePO = FileParser.parse_file(request)
        file_grid_po: FileGridPO = FileParser.parse_file_grid(request.file, file_po.id)
        FileService.insert_one(file_po)
        FileGridService.insert_one(file_grid_po)

        return file_po

    @classmethod
    def upload(cls, request: FileUploadRequest) -> FileDetailVO:
        """ 文件 | 上传 """

        FileValidator.validate_create(request)
        file_po: FilePO = cls._create_data(request)
        cls.version_log(file_po)
        vo: FileDetailVO = FileMapping.po_2_detail_vo(file_po)

        return vo


class FileDeleter(DeleterBase):
    """ 文件 | 删除器 """

    @staticmethod
    def _delete_data(request: FileDetailRequest) -> FilePO:
        """ 删除数据 """

        old_file_po: FilePO = FileService.get_one_by_id(request.file_id)
        new_file_po: FilePO = old_file_po.model_copy(deep=True)
        FileService.delete_one(new_file_po)

        return old_file_po

    @classmethod
    def delete(cls, request: FileDetailRequest) -> None:
        """ 删除文件 """

        FileValidator.validate_delete(request)
        file_po: FilePO = cls._delete_data(request)
        cls.version_log(file_po)


class FileUpdator(UpdatorBase):
    """ 文件 | 更新器 """

    @staticmethod
    def _update_data(request: FileUpdateRequest) -> tuple[FilePO, FilePO]:
        """ 更新数据 """

        old_file_po: FilePO = FileService.get_one_by_id(request.file_id)
        new_file_po: FilePO = old_file_po.model_copy(deep=True)
        new_file_po.update_from_request(request)
        FileService.update_one(new_file_po)

        return old_file_po, new_file_po

    @classmethod
    def update(cls, request: FileUpdateRequest) -> FileDetailVO:
        """ 更新文件 """

        FileValidator.validate_update(request)
        old_file_po, new_file_po = cls._update_data(request)
        cls.version_log(old_file_po, new_file_po)
        vo: FileDetailVO = FileMapping.po_2_detail_vo(new_file_po)

        return vo


class FileGridGetter:
    """ 文件存储 | 获取器 """

    @classmethod
    def get_file_grid(cls, request: FileDetailRequest) -> FileStream:
        """ 获取文件存储 """

        FileValidator.validate_exist(request.file_id)
        file_po: FilePO = FileService.get_one_by_id(request.file_id)
        file_frid_po: FileGridPO = FileGridService.get_one_by_id(request.file_id)

        return FileStream(
            content=file_frid_po.content,
            filename=file_po.raw_filename,
            media_type=file_po.content_type,
        )


class FileDefaultHandler:
    """ 文件 | 默认 | 处理器 """

    class DefaultAvatar:
        """ 默认头像 """

        remark: str = "默认头像"
        filename: str = "default_avatar.jpg"
        path: Path = settings.PATH.STATIC_DIR / "img" / filename
        content_type: str = "image/jpeg"

    @classmethod
    def set_default_avatar_file(cls) -> FilePO:
        """ 设置默认头像文件 """

        with open(cls.DefaultAvatar.path, "rb") as f:
            file = BytesIO(f.read())

        upload_file = UploadFile(
            file=file,
            filename=cls.DefaultAvatar.filename,
            size=len(file.getbuffer()),
            headers=Headers({"content-type": cls.DefaultAvatar.content_type}),
        )
        request = FileUploadRequest(file=upload_file)
        file_po: FilePO = FileParser.parse_file(request)
        file_po.remark = cls.DefaultAvatar.remark
        file_po.is_default = True
        file_grid_po: FileGridPO = FileParser.parse_file_grid(request.file, file_po.id)
        FileService.insert_one(file_po)
        FileGridService.insert_one(file_grid_po)
        log.success(f"[文件][默认] 已设置默认头像：{file_po.id}")

        return file_po

    @classmethod
    def get_default_avatar_file(cls) -> FilePO:
        """ 获取默认头像文件 """

        if file_po := FileService.get_one_by_remark(cls.DefaultAvatar.remark):
            return file_po

        return cls.set_default_avatar_file()

    @staticmethod
    def get_avatar_url(avatar_id: ObjectIdStr) -> str:
        """ 获取头像预览 URL """

        return FileGetter.get_file_view_url(avatar_id)


class FileZipHandler:
    """
    文件 | 压缩 | 处理器

    0. 生成 ProcessPO（前置操作）
    1. 拿到 FilePO、FileGridPO 列表（ProcessPO 进度：10）
    2. 将文件写到指定的文件夹 XXX 中（ProcessPO 进度：20）
    3. 压缩文件夹为 XXX.zip（ProcessPO 进度：30~70）
    4. 将 XXX.zip 上传到文件存储中 FilePO（ProcessPO 进度：80）
    5. 删除 文件夹 XXX、XXX.zip（ProcessPO 进度：90）
    6. 下载 URL 放入 ProcessPO 的 result（ProcessPO 进度：100）
    """

    @staticmethod
    def parse_file_list(
        file_po_list: list[FilePO],
        file_grid_po_list: list[FileGridPO],
    ) -> list[tuple[str, bytes]]:
        """ 解析文件列表 """

        if not (
            file_po_list
            and file_grid_po_list
        ):
            return []

        filename_list: ListStr = []
        parsed_file_list: list[tuple[str, bytes]] = []
        id_2_file_po: dict[ObjectIdStr, FilePO] = {
            file_po.id: file_po
            for file_po in file_po_list
        }

        for file_grid_po in file_grid_po_list:
            file_po: FilePO | None = id_2_file_po[file_grid_po.id]

            if not file_po:
                continue

            if file_po.raw_filename not in filename_list:
                filename = file_po.raw_filename

            else:
                *_raw_name, _suffix = file_po.raw_filename.split(".")
                _count: int = filename_list.count(file_po.raw_filename)
                filename = f"{'.'.join(_raw_name)}({_count}).{_suffix}"

            filename_list.append(file_po.raw_filename)
            parsed_file_list.append((filename, file_grid_po.content))

        return parsed_file_list

    @staticmethod
    def write_file_to_directory(
        file_list: list[tuple[str, bytes]],
        directory: Path,
    ) -> None:
        """ 将文件写到指定的文件夹中 """

        if not directory.exists():
            directory.mkdir(parents=True)

        for filename, content in file_list:
            with open(directory / filename, "wb") as f:
                f.write(content)

    @staticmethod
    def remove_file(path: Path) -> None:
        """ 删除文件/文件夹 """

        if not path.exists():
            return

        if path.is_dir():
            shutil.rmtree(path)

        elif path.is_file():
            path.unlink()

    @staticmethod
    def upload_file(
        zip_file_path: Path,
        zip_file_name: str,
    ) -> FilePO:
        """ 上传压缩的文件 """

        with open(zip_file_path, "rb") as f:
            file = BytesIO(f.read())

        upload_file = UploadFile(
            file=file,
            filename=zip_file_name,
            size=len(file.getbuffer()),
            headers=Headers({"content-type": enum.ContentType.ZIP}),
        )
        request = FileUploadRequest(file=upload_file)
        file_po: FilePO = FileParser.parse_file(request)
        file_po.remark = "系统压缩的文件"
        file_grid_po: FileGridPO = FileParser.parse_file_grid(request.file, file_po.id)
        FileService.insert_one(file_po)
        FileGridService.insert_one(file_grid_po)

        return file_po

    @classmethod
    @async_io_task
    @task_exc_catcher(logger=log)
    def async_file_zip_and_upload(
        cls,
        process_po: ProcessPO,
        file_po_list: list[FilePO],
        file_grid_po_list: list[FileGridPO],
    ) -> None:
        """ 文件压缩并上传 """

        from biz.process_biz import ProcessHandler

        user_id: ObjectIdStr = get_user_id()
        log.info(f"[文件][批量下载][压缩上传] 开始 user_id: {user_id} process_id: {process_po.id}")
        now_str: str = DATETIME.now_str().replace(":", " ")
        root: Path = settings.PATH.ZIP_FILE_DIR / process_po.id
        directory: Path = root / f"{now_str} 批量下载文件夹"
        zip_file_name: str = f"{now_str} 批量下载文件.zip"
        zip_file_path: Path = root / zip_file_name

        try:
            # 1. 拿到 FilePO、FileGridPO 列表（ProcessPO 进度：10）
            file_list: list[tuple[str, bytes]] = cls.parse_file_list(file_po_list, file_grid_po_list)

            _update_process = partial(ProcessHandler.update_process, process_id=process_po.id)
            _finish_process = partial(ProcessHandler.finish_process, process_id=process_po.id)

            _update_process(percent=10)

            # 2. 将文件写到指定的文件夹 XXX 中（ProcessPO 进度：20）
            cls.write_file_to_directory(file_list, directory)
            _update_process(percent=20)

            # 3. 压缩文件夹为 XXX.zip（ProcessPO 进度：30~70）
            zip_directory(directory, zip_file_path)
            _update_process(percent=70)

            # 4. 将 XXX.zip 上传到文件存储中 FilePO（ProcessPO 进度：80）
            file_po: FilePO = cls.upload_file(zip_file_path, zip_file_name)
            _update_process(percent=80)

            # 5. 删除 文件夹 XXX、XXX.zip（ProcessPO 进度：90）
            cls.remove_file(root)
            _update_process(percent=90)

            # 6. 下载 URL 放入 ProcessPO 的 result（ProcessPO 进度：100）
            download_url: str = FileGetter.get_file_download_url(file_po.id)
            _finish_process(result={"download_url": download_url, "file_id": file_po.id})
            log.info(f"[文件][批量下载][压缩上传] 结束 user_id: {user_id} process_id: {process_po.id}")

        except ValueError as e:
            log.error(
                f"[文件][批量下载][压缩上传] 已知异常 user_id: {user_id} "
                f"process_id: {process_po.id} traceback: {e}"
            )
            ProcessHandler.exception_process(process_po, exception_msg="导出失败")

        except Exception as _:  # noqa: ignore[broad exception]
            log.error(
                f"[文件][批量下载][压缩上传] 未知异常 user_id: {user_id} "
                f"process_id: {process_po.id} traceback: {traceback.format_exc()}"
            )
            ProcessHandler.exception_process(process_po, exception_msg="导出失败")


class FileBatchDownloadHandler:
    """ 文件 | 批量下载 | 处理器 """

    @classmethod
    def batch_download(cls, request: FileBatchDownloadRequest) -> FileBatchDownloadVO:
        """ 批量下载文件 """

        from biz.process_biz import ProcessHandler

        FileValidator.validate_batch_download(request)
        process_po: ProcessPO = ProcessHandler.init_process(task=enum.ProcessTask.FILE_BATCH_DOWNLOAD)
        file_po_list: Iterator[FilePO] = FileService.get_many_by_id_list(request.file_id_list)
        file_grid_po_list: Iterator[FileGridPO] = FileGridService.get_many_by_id_list(request.file_id_list)
        FileZipHandler.async_file_zip_and_upload(process_po, list(file_po_list), list(file_grid_po_list))

        return FileBatchDownloadVO(process_id=process_po.id)


if __name__ == "__main__":
    # 获取默认头像文件
    _file_po = FileDefaultHandler.get_default_avatar_file()
    print(FileDefaultHandler.get_avatar_url(_file_po.id))

    # # 文件压缩上传
    # from biz.process_biz import ProcessHandler
    #
    # _process_po = ProcessHandler.init_process(task="文件压缩上传")
    # id_list = [
    #     ObjectIdStr("66b572ca0d174524f902ba3d"),
    #     ObjectIdStr("66bab7c6bfad94bd1f665a4f"),
    #     ObjectIdStr("66bac40e46d4171af3cb7e67"),
    # ]
    #
    # _file_po_list = FileService.get_many_by_id_list(id_list)
    # _file_grid_po_list = FileGridService.get_many_by_id_list(id_list)
    # FileZipHandler.file_zip_and_upload(_process_po, _file_po_list, _file_grid_po_list)
